package com.hqd.ch03.v23.aop.config;

import com.hqd.ch03.v23.aop.framework.aspectj.*;
import com.hqd.ch03.v23.aop.support.DefaultBeanFactoryPointcutAdvisor;
import com.hqd.ch03.v23.config.AopNamespaceUtils;
import com.hqd.ch03.v23.config.BeanDefinition;
import com.hqd.ch03.v23.factory.ConfigurableBeanFactory;
import com.hqd.ch03.v23.reader.xml.ParserContext;
import com.hqd.ch03.v23.reader.xml.parse.BeanDefinitionParser;
import com.hqd.ch03.v23.utils.DomUtils;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Element;

import java.util.List;

public class ConfigBeanDefinitionParser implements BeanDefinitionParser {
    private static final String ASPECT = "aspect";
    private static final String EXPRESSION = "expression";
    private static final String ID = "id";
    private static final String POINTCUT = "pointcut";
    private static final String ADVICE_BEAN_NAME = "adviceBeanName";
    private static final String ADVISOR = "advisor";
    private static final String ADVICE_REF = "advice-ref";
    private static final String POINTCUT_REF = "pointcut-ref";
    private static final String REF = "ref";
    private static final String BEFORE = "before";
    private static final String DECLARE_PARENTS = "declare-parents";
    private static final String TYPE_PATTERN = "types-matching";
    private static final String DEFAULT_IMPL = "default-impl";
    private static final String DELEGATE_REF = "delegate-ref";
    private static final String IMPLEMENT_INTERFACE = "implement-interface";
    private static final String AFTER = "after";
    private static final String AFTER_RETURNING_ELEMENT = "after-returning";
    private static final String AFTER_THROWING_ELEMENT = "after-throwing";
    private static final String AROUND = "around";
    private static final String RETURNING = "returning";
    private static final String RETURNING_PROPERTY = "returningName";
    private static final String THROWING = "throwing";
    private static final String THROWING_PROPERTY = "throwingName";
    private static final String ARG_NAMES = "arg-names";
    private static final String ARG_NAMES_PROPERTY = "argumentNames";
    private static final String ASPECT_NAME_PROPERTY = "aspectName";
    private static final String DECLARATION_ORDER_PROPERTY = "declarationOrder";
    private static final String ORDER_PROPERTY = "order";

    private static final String ADVICE_METHOD_PROPERTY = "aspectJAdviceMethod";
    private static final String ADVICE_POINTCUT_PROPERTY = "pointcut";
    private static final String ADVICE_ASPECT_PROPERTY = "aspectInstance";

    private static final String ADVISOR_ADVICE_PROPERTY = "advice";

    @Override
    public BeanDefinition parse(Element element, ParserContext context) {
        configureAutoProxyCreator(context, element);

        List<Element> childElements = DomUtils.getChildElements(element);
        childElements.forEach(child -> {
            String localName = child.getLocalName();
            //解析切点
            if (POINTCUT.equals(localName)) {
                parsePointcut(child, context);
            } else if (ADVISOR.equals(localName)) {//解析切面
                parseAdvisor(child, context);
            } else if (ASPECT.equals(localName)) {//解析通知
                parseAspect(child, context);
            }
        });
        return null;
    }

    private void configureAutoProxyCreator(ParserContext parserContext, Element element) {
        AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(parserContext, element);
    }

    private void parseAdvisor(Element advisorElement, ParserContext parserContext) {
        BeanDefinition advisorDef = createAdvisorBeanDefinition(advisorElement, parserContext);
        String id = advisorElement.getAttribute(ID);

        String advisorBeanName = id;
        if (StringUtils.isNotBlank(advisorBeanName)) {
            parserContext.registerBeanDefinition(advisorBeanName, advisorDef);
        } else {
            parserContext.registerWithGeneratedName(advisorDef);
        }

        Object pointcut = parsePointcutProperty(advisorElement);
        if (pointcut instanceof BeanDefinition) {
            String pbdn = parserContext.registerWithGeneratedName(advisorDef);
            advisorDef.getPropertyValues().add(POINTCUT, pbdn, true);
        } else if (pointcut instanceof String) {
            advisorDef.getPropertyValues().add(POINTCUT, ((String) pointcut), true);
        }
    }

    private BeanDefinition createAdvisorBeanDefinition(Element advisorElement, ParserContext parserContext) {
        BeanDefinition advisorDefinition = new BeanDefinition(DefaultBeanFactoryPointcutAdvisor.class);

        /**
         * 获取通知类
         */
        String adviceRef = advisorElement.getAttribute(ADVICE_REF);
        if (StringUtils.isBlank(adviceRef)) {
            throw new RuntimeException("'advice-ref' attribute contains empty value.");
        } else {
            advisorDefinition.getPropertyValues().add(
                    ADVICE_BEAN_NAME, adviceRef, true);
        }

        if (advisorElement.hasAttribute(ORDER_PROPERTY)) {
            advisorDefinition.getPropertyValues().add(
                    ORDER_PROPERTY, advisorElement.getAttribute(ORDER_PROPERTY));
        }

        return advisorDefinition;
    }

    private void parseAspect(Element aspectElement, ParserContext context) {
        String id = aspectElement.getAttribute(ID);
        String aspectName = aspectElement.getAttribute(REF);
        List<Element> childElements = DomUtils.getChildElements(aspectElement);
        for (int i = 0; i < childElements.size(); i++) {
            Element element = childElements.get(i);
            /**
             * 是否是通知节点
             */
            if (isAdviceNode(element)) {
                parseAdvice(
                        aspectName, i, aspectElement, element, context);
            }
        }
        List<Element> pointcuts = DomUtils.getChildElementsByTagName(aspectElement, POINTCUT);
        for (Element pointcutElement : pointcuts) {
            parsePointcut(pointcutElement, context);
        }

    }


    private BeanDefinition parseAdvice(String aspectName, int order, Element aspectElement,
                                       Element adviceElement, ParserContext context) {
        /**
         * public AspectJAfterAdvice(AspectJExpressionPointcut pointcut, Method aspectJAdviceMethod, Object aspect)
         * 刚处于解析阶段，无法获取相应的对象，用FactoryBean延迟加载
         */
        BeanDefinition methodDefinition = new BeanDefinition(MethodLocatingFactoryBean.class);
        methodDefinition.getPropertyValues().addProperty("targetBeanName", aspectName);
        methodDefinition.getPropertyValues().addProperty("methodName", adviceElement.getAttribute("method"));
        methodDefinition.setSynthetic(true);
        context.registerWithGeneratedName(methodDefinition);


        /**
         * 创建aspect对象工厂
         */
        BeanDefinition aspectFactoryDef =
                new BeanDefinition(SimpleBeanFactoryAwareAspectInstanceFactory.class);
        aspectFactoryDef.getPropertyValues().addProperty("aspectBeanName", aspectName);
        aspectFactoryDef.setSynthetic(true);
        context.registerWithGeneratedName(aspectFactoryDef);

        BeanDefinition adviceDefinition = createAdviceDefinition(
                adviceElement, aspectName, order, methodDefinition, aspectFactoryDef, context);
        context.registerWithGeneratedName(adviceDefinition);

        BeanDefinition advisorDefinition = new BeanDefinition(AspectJPointcutAdvisor.class);
        advisorDefinition.getPropertyValues().add(ADVISOR_ADVICE_PROPERTY, adviceDefinition.getId(), true);
        if (aspectElement.hasAttribute(ORDER_PROPERTY)) {
            advisorDefinition.getPropertyValues().add(
                    ORDER_PROPERTY, aspectElement.getAttribute(ORDER_PROPERTY));
        }
        context.registerWithGeneratedName(advisorDefinition);
        return advisorDefinition;
    }

    private BeanDefinition createAdviceDefinition(Element adviceElement, String aspectName, int order,
                                                  BeanDefinition methodDef, BeanDefinition aspectFactoryDef, ParserContext context) {

        BeanDefinition adviceDefinition = new BeanDefinition(getAdviceClass(adviceElement));

        adviceDefinition.getPropertyValues().add(ASPECT_NAME_PROPERTY, aspectName);
        adviceDefinition.getPropertyValues().add(DECLARATION_ORDER_PROPERTY, String.valueOf(order));

        /**
         * 是否保护return
         */
        if (adviceElement.hasAttribute(RETURNING)) {
            adviceDefinition.getPropertyValues().add(
                    RETURNING_PROPERTY, adviceElement.getAttribute(RETURNING));
        }
        /**
         * 是否包含异常
         */
        if (adviceElement.hasAttribute(THROWING)) {
            adviceDefinition.getPropertyValues().add(
                    THROWING_PROPERTY, adviceElement.getAttribute(THROWING));
        }
        /**
         * 是否包含args
         */
        if (adviceElement.hasAttribute(ARG_NAMES)) {
            adviceDefinition.getPropertyValues().add(
                    ARG_NAMES_PROPERTY, adviceElement.getAttribute(ARG_NAMES));
        }

        /**
         * 增强方法
         */
        adviceDefinition.getPropertyValues().add(ADVICE_METHOD_PROPERTY, methodDef.getId(), true);
        /**
         * 切点
         */
        Object pointcut = parsePointcutProperty(adviceElement);
        if (pointcut instanceof BeanDefinition) {
            String pbdn = context.registerWithGeneratedName((BeanDefinition) pointcut);
            adviceDefinition.getPropertyValues().add(ADVICE_POINTCUT_PROPERTY, pbdn, true);

        } else if (pointcut instanceof String) {
            adviceDefinition.getPropertyValues().add(ADVICE_POINTCUT_PROPERTY, (String) pointcut, true);
        }

        adviceDefinition.getPropertyValues().add(ADVICE_ASPECT_PROPERTY, aspectFactoryDef.getId(), true);
        return adviceDefinition;

    }

    private Object parsePointcutProperty(Element element) {
        if (element.hasAttribute(POINTCUT) && element.hasAttribute(POINTCUT_REF)) {
            throw new RuntimeException(
                    "Cannot define both 'pointcut' and 'pointcut-ref' on <advisor> tag.");
        } else if (element.hasAttribute(POINTCUT)) {
            String expression = element.getAttribute(POINTCUT);
            return createPointcutDefinition(expression);
        } else if (element.hasAttribute(POINTCUT_REF)) {
            String pointcutRef = element.getAttribute(POINTCUT_REF);
            if (StringUtils.isBlank(pointcutRef)) {
                throw new RuntimeException("'pointcut-ref' attribute contains empty value.");
            }
            return pointcutRef;
        } else {
            throw new RuntimeException("Must define one of 'pointcut' or 'pointcut-ref' on <advisor> tag.");
        }
    }

    private boolean isAdviceNode(Element element) {
        String name = element.getLocalName();
        return (BEFORE.equals(name) || AFTER.equals(name) || AFTER_RETURNING_ELEMENT.equals(name) ||
                AFTER_THROWING_ELEMENT.equals(name) || AROUND.equals(name));
    }

    /**
     * 获取通知类型
     *
     * @param adviceElement
     * @return
     */
    private Class<?> getAdviceClass(Element adviceElement) {
        String elementName = adviceElement.getLocalName();
        if (BEFORE.equals(elementName)) {
            return AspectJMethodBeforeAdvice.class;
        } else if (AFTER.equals(elementName)) {
            return AspectJAfterAdvice.class;
        } else if (AFTER_RETURNING_ELEMENT.equals(elementName)) {
            return AspectJAfterReturningAdvice.class;
        } else if (AFTER_THROWING_ELEMENT.equals(elementName)) {
            return AspectJAfterThrowingAdvice.class;
        } else if (AROUND.equals(elementName)) {
            return AspectJAroundAdvice.class;
        } else {
            throw new IllegalArgumentException("Unknown advice kind [" + elementName + "].");
        }
    }

    /**
     * 转换pointcut节点
     *
     * @param pointcutElement
     * @param context
     * @return
     */
    private BeanDefinition parsePointcut(Element pointcutElement, ParserContext context) {
        String id = pointcutElement.getAttribute(ID);
        String expression = pointcutElement.getAttribute(EXPRESSION);


        /**
         * 1、创建切点BeanDefinition
         * 2、注册切点BeanDefinition
         */
        BeanDefinition pointcutDefinition = createPointcutDefinition(expression);
        pointcutDefinition.setId(id);
        String pointcutBeanName = id;
        if (StringUtils.isNotBlank(pointcutBeanName)) {
            context.registerBeanDefinition(pointcutBeanName, pointcutDefinition);
        } else {
            context.registerWithGeneratedName(pointcutDefinition);
        }

        return pointcutDefinition;
    }

    protected BeanDefinition createPointcutDefinition(String expression) {
        /**
         * 此处设置多利，避免线程安全问题
         */
        BeanDefinition beanDefinition = new BeanDefinition(AspectJExpressionPointcut.class);
        beanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
        beanDefinition.getPropertyValues().addProperty(EXPRESSION, expression);
        return beanDefinition;
    }
}
